package de.jmda.core.util.classpath;

import static java.lang.System.lineSeparator;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;

import org.apache.log4j.Logger;

import de.jmda.core.util.RuntimeUtil;
import de.jmda.core.util.ramcomp.RAMCompiler;
import de.jmda.core.util.ramcomp.RAMCompiler.RAMCompilerData;

public abstract class ClasspathUtil
{
	private final static Logger LOGGER =
      Logger.getLogger(ClasspathUtil.class);

	public final static FileFilter FILTER_ACCEPT_FILES_ALL =
			new FileFilter()
			{
				@Override
				public boolean accept(File file) { return true; }
			};

  public final static FileFilter FILTER_ACCEPT_FILES_ALL_BUT_JAR =
			new FileFilter()
			{
				@Override
				public boolean accept(File file)
				{ return file.getName().endsWith(".jar") ? false : true ; }
			};

	public final static FileFilter FILTER_ACCEPT_FILES_EXISTING_ONLY =
			new FileFilter()
			{
				@Override
				public boolean accept(File file) { return file.exists(); }
			};

	public final static FileFilter FILTER_ACCEPT_FILES_DIRECTORIES_ONLY =
			new FileFilter()
			{
				@Override
				public boolean accept(File file) { return file.isDirectory(); }
			};

  public final static ZipEntryFilter FILTER_ACCEPT_ZIP_ENTRIES_ALL =
      new ZipEntryFilter()
			{
				@Override
				public boolean accept(ZipEntry zipEntry) { return true; }
			};

  public final static ZipEntryFilter FILTER_ACCEPT_ZIP_ENTRIES_DIRECTORIES_ONLY =
      new ZipEntryFilter()
			{
				@Override
				public boolean accept(ZipEntry zipEntry) { return zipEntry.isDirectory(); }
			};

	public static Map<String, ClasspathResource> getClasspathResources()
			throws IOException
	{
		return
				getClasspathResources(
						FILTER_ACCEPT_FILES_ALL,
						FILTER_ACCEPT_FILES_ALL,
						FILTER_ACCEPT_ZIP_ENTRIES_ALL);
	}

	/**
	* @param resourceContainerFilter filter for resource containers listed in
	*                                <code>System.getProperty("java.class.path")
	*                                </code>, use this filter to narrow the
	*                                entries in the java class path
	* @param directoryResourceFilter filter for resource files in <code>
	*                                System.getProperty("java.class.path")</code>
	*                                directories, use this filter to narrow the
	*                                resources of the file system directories in
	*                                the java class path
	* @param zipResourceFilter filter for jar resource files in <code>
	*                          System.getProperty("java.class.path")</code> .jar
	*                          files, use this filter to narrow the entries of a
	*                          .jar file
	* @return resources in <code>System.getProperty("java.class.path")</code> if
	*         they are accepted by all filter parameters
	* @throws IOException 
	*/
	public static Map<String, ClasspathResource> getClasspathResources(
			FileFilter resourceContainerFilter,
			FileFilter directoryResourceFilter,
			ZipEntryFilter zipResourceFilter)
			throws IOException
	{
		Map<String, ClasspathResource> result = new HashMap<>();

		// collection of all ResourceContainers which's files are accepted by
		// resourceContainerFilter
		List<ResourceContainer> resourceContainers = new ArrayList<>();

		String[] resourceContainerNames =
				RuntimeUtil.JAVA_CLASS_PATH.split(RuntimeUtil.PATH_SEPARATOR);
		
		for (String resourceContainerName : resourceContainerNames)
		{
			File resourceContainerFile = new File(resourceContainerName);
			
			if (resourceContainerFilter.accept(resourceContainerFile))
			{
				ResourceContainer resourceContainer;
				
				if (resourceContainerFile.isDirectory())
				{
					resourceContainer =
							new ResourceContainerDirectory(resourceContainerFile);
				}
				else
				{
					resourceContainer =
							new ResourceContainerJarFile(resourceContainerFile);
				}
				
				resourceContainers.add(resourceContainer);
			}
		}
		
		// collect resources from all ResourceContainers
		for (ResourceContainer resourceContainer : resourceContainers)
		{
			if (resourceContainer instanceof ResourceContainerDirectory)
			{
				result.putAll
				(
						(
								(ResourceContainerDirectory) resourceContainer
						).getClassPathResources(directoryResourceFilter)
				);
			}
			else if (resourceContainer instanceof ResourceContainerJarFile)
			{
				result.putAll
				(
						(
								(ResourceContainerJarFile) resourceContainer
						).getClassPathResources(zipResourceFilter)
				);
			}
			else
			{
				System.err.println(
						"unexpected resource container type " +
						resourceContainer.getClass().getName());
			}
		}

		return result;
	}

	public static Map<String, ClasspathResource> getPackagesAsClasspathResources()
			throws IOException
	{
		return
				getClasspathResources(
						FILTER_ACCEPT_FILES_ALL,
						FILTER_ACCEPT_FILES_DIRECTORIES_ONLY,
						FILTER_ACCEPT_ZIP_ENTRIES_DIRECTORIES_ONLY);
	}

	public static Map<String, Package> getPackages(Set<String> packagenames)
			throws
					IOException,
					InstantiationException,
					IllegalAccessException,
					ClassNotFoundException
	{
		Map<String, Package> result = new HashMap<>();

		Set<String> unresolvedPackagenames = new HashSet<>();

		for (String packagename : packagenames)
		{
			if (packagename.contains("-") ||  // package names must not contain '-'
			    packagename.contains("enum")) // package names must not contain "enum"
			{
				LOGGER.debug("skipping package " + packagename);
				continue;
			}

			// convert resource style package name (containing '/') to package naming
			// style (containing '.' instead of '/')
			packagename = packagename.replace('/', '.');
			// convert .jar style package name (containing '\\') to package naming
			// style (containing '.' instead of '\\')
			packagename = packagename.replace('\\', '.');

			Package pckg = Package.getPackage(packagename);
			
			if (pckg == null)
			{
				unresolvedPackagenames.add(packagename);
			}
			else
			{
				result.put(packagename, pckg);
			}
		}
		
		List<RAMCompilerData> compilerDataItems =
				createRAMCompilerDataItems(unresolvedPackagenames);

		RAMCompiler.compile(compilerDataItems);
		
		for (RAMCompilerData item : compilerDataItems)
		{
			Object object =
					item.getClassLoader()
					    .loadClass(item.getQualifiedTypeName()).newInstance();
			
			Class<? extends Object> clazz = object.getClass();

			Package pckg = clazz.getPackage();
			
			if (pckg == null)
			{
				LOGGER.warn(
						"failure creating " + Package.class.getName() + " object for " +
						item.getQualifiedTypeName());
			}
			else
			{
				result.put(item.getQualifiedTypeName(), pckg);
			}
		}

		return result;
	}

	private static List<RAMCompilerData> createRAMCompilerDataItems(
			Set<String> packagenames)
  {
		List<RAMCompilerData> result = new ArrayList<>();

		for (String packagename : packagenames)
		{
			result.add(createRAMCompilerDataItem(packagename));
		}
		
		return result;
  }

	private static RAMCompilerData createRAMCompilerDataItem(String packagename)
  {
		String typename = "__DynaClass__";
		
		String source =
				"package " + packagename + ";" + lineSeparator() +
				"public class " + typename     + lineSeparator() +
				"{"                            + lineSeparator() +
				"}";

		return new RAMCompilerData(packagename + "." + typename, source);
  }
}